package daybook.io;

import daybook.Resources;
import daybook.exception.ApplicationException;
import daybook.exception.InvalidException;
import daybook.exception.NotFoundException;
import daybook.model.Account;
import daybook.model.ChartOfAccounts;
import daybook.model.Commodity;
import daybook.model.transaction.VirtualAccount;
import daybook.util.Utils;
import org.json.JSONArray;
import org.json.JSONObject;

import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public final class SimpleExpression {
    private SimpleExpression() {}

    public static <T> String toString(Collection<T> values, Function<T, String> toString) {
        if (values == null) {
            return "all";
        } else if (values.isEmpty()) {
            return "none";
        } else {
            return values.stream()
                    .map(toString)
                    .collect(Collectors.joining(","));
        }
    }

    // for log
    public static String accountsToString(Collection<Account> accounts) {
        return toString(accounts, account -> account.getName() + " (" + account.getCode() + ")");
    }

    // for log
    public static String commoditiesToString(Collection<Commodity> commodities) {
        return toString(commodities, Commodity::getName);
    }

    private static <T> Set<T> toSet(Stream<String> exps, Function<String, Set<T>> compiler) {
        return exps.map(compiler).flatMap(Set::stream).collect(Collectors.toSet());
    }

    private static <T> Set<T> negate(Set<T> set, Supplier<Set<T>> universal) {
        if (set == null) {
            return Collections.emptySet();
        } else if (set.isEmpty()) {
            return null;
        } else {
            final Set<T> result = universal.get();
            result.removeAll(set);
            return result;
        }
    }

    public static <T> Set<T> toSet(List<String> exps, boolean negative, Function<String, Set<T>> compiler, Supplier<Set<T>> universal) {
        final Set<T> set = exps == null ? null: toSet(exps.stream(), compiler);
        return negative ? negate(set, universal) : set;
    }

    public static <T> Set<T> toSet(Object json, Function<String, Set<T>> compiler, Supplier<Set<T>> universal) {
        if (json == null) {
            return null;
        } else if (json instanceof String) {
            if (json.toString().toLowerCase().equals("all")) {
                return null;
            } else {
                throw InvalidException.of(Resources.ErrorMessages.INVALID_EXPRESSION, json);
            }
        } else if (json instanceof JSONArray) {
            return toSet(Utils.toStream((JSONArray) json, JSONArray::getString), compiler);
        } else if (json instanceof JSONObject) {
            final JSONArray array = ((JSONObject) json).getJSONArray("not");
            final Set<T> set = toSet(Utils.toStream(array, JSONArray::getString), compiler);
            return negate(set, universal);
        } else {
            throw InvalidException.of(Resources.ErrorMessages.INVALID_EXPRESSION, json);
        }
    }

    public static Function<String, Set<Commodity>> getCommodityExpressionCompiler(Map<String, Commodity> commodities) {
        return exp -> {
            final Commodity result = commodities.get(exp);
            if (result == null) {
                throw NotFoundException.of(Resources.ErrorMessages.COMMODITY_NOT_FOUND);
            }
            return Collections.singleton(result);
        };
    }

    public static Function<String, Set<Account>> getAccountExpressionCompiler(ChartOfAccounts chartOfAccounts) {
        return exp -> {
            try {
                final VirtualAccount va = VirtualAccount.of(exp, chartOfAccounts);
                if (va.getTags().isPresent()) {
                    throw InvalidException.of(Resources.ErrorMessages.INVALID_ACCOUNT_EXPRESSION, exp);
                }
                return va.getAccounts().orElse(Collections.emptySet());
            } catch (IllegalArgumentException | ApplicationException e) {
                throw InvalidException.of(e, Resources.ErrorMessages.INVALID_ACCOUNT_EXPRESSION, exp);
            }
        };
    }

    public static Supplier<Set<Commodity>> getCommodityUniversal(Map<String, Commodity> commodities) {
        return () -> new HashSet<>(commodities.values());
    }

    public static Supplier<Set<Account>> getAccountUniversal(ChartOfAccounts chartOfAccounts) {
        return () -> chartOfAccounts.getTree().leafStream().collect(Collectors.toSet());
    }

    public static Set<Account> accountExpressionToSet(List<String> exps, boolean negative, ChartOfAccounts chartOfAccounts) {
        return toSet(exps, negative, getAccountExpressionCompiler(chartOfAccounts), getAccountUniversal(chartOfAccounts));
    }

    public static Set<Commodity> commodityExpressionToSet(List<String> exps, boolean negative, Map<String, Commodity> commodities) {
        return toSet(exps, negative, getCommodityExpressionCompiler(commodities), getCommodityUniversal(commodities));
    }

    public static Set<Account> accountExpressionToSet(Object json, ChartOfAccounts chartOfAccounts) {
        return toSet(json, getAccountExpressionCompiler(chartOfAccounts), getAccountUniversal(chartOfAccounts));
    }

    public static Set<Commodity> commodityExpressionToSet(Object json, Map<String, Commodity> commodities) {
        return toSet(json, getCommodityExpressionCompiler(commodities), getCommodityUniversal(commodities));
    }
}
